﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using UnityEngine;

namespace UnityEngine
{
	#if UNITY_XENON_API && ENABLE_XENON_SOCIALAPI
	// TODO: We have no concept of achievement types like xbox has, not exposed
	// TODO: No concept of user privilege levels, not exposed
	// NOTE: There doesn't seem to be any way to explicitly set your own state (online/offline/away/busy/playing)
	public class XboxLive : ISocial
	{
		private static LocalUser s_LocalUser;
		private Action<AchievementDescription[]> m_AchievementDescriptionCallback;
		private Action<bool> m_AchievementReportingCallback;

		private static GameObject s_SessionObject;
		private static GameObject s_LeaderboardRoutine;
		
		internal const uint kDefaultUserIndex = 0;

		public static bool onlineMode { get; set; }

		public XboxLive()
		{
			if (X360Core.IsUserSignedIn(0, true))
			{
				Debug.Log("User already signed in, in online mode");
				onlineMode = true;
			}
			
			// DEBUG: Log when these callbacks are triggered but have not been set elsewhere
			X360Achievements.OnAchievementsEnumerated = () => { Debug.Log("OnAchievementsEnumerated called"); };
			X360Achievements.OnUserAchievementsUpdated = (id) => { Debug.Log("OnUserAchievementsUpdated called for user " + id); };
			X360Achievements.OnAward =
				(uid, aid, status) => { Debug.Log("OnAward called for user=" + uid + " achievement=" + aid + " status=" + status); };
		}

		public void ShowAchievementsUI()
		{
			X360Achievements.ShowUI(kDefaultUserIndex);
		}

		// TODO: These should include an action which is triggered (OnSystemUIVisibilityChange) when the UI state changes (UI is dismissed)
		public void ShowAchievementsUI(uint userIndex)
		{
			X360Achievements.ShowUI(userIndex);
		}

		public void ShowLeaderboardUI()
		{
			Debug.Log("Not implemented");
		}

		public void ShowFriendsUI(uint userIndex)
		{
			X360Friends.ShowFriendsUI(userIndex);
		}

		// NOTE: localUser.underage is not implemented on Xbox Live
		public LocalUser localUser
		{
			get
			{
				if (s_LocalUser == null)
					s_LocalUser = new LocalUser();
				GetLocalUser(kDefaultUserIndex, ref s_LocalUser);
				return s_LocalUser;
			}
		}

		// TODO: Cache user so he's not recreated every time
		// TODO: Maybe set this up as a generic list, the list is populated as users are logged in and valid, no need to create on access, no need for seperate GetCount() type functions
		public LocalUser this[uint index]
		{
			get
			{
				LocalUser user = new LocalUser();
				GetLocalUser(index, ref user);
				return user;
			}
		}

		private void GetLocalUser(uint index, ref LocalUser user)
		{
			user.m_Authenticated = X360Core.IsUserSignedIn(index, onlineMode);
			if (!user.m_Authenticated)
			{
				//Debug.Log("Must be logged in before getting local user details");
				return;
			}
			user.m_UserName = X360Core.GetUserName(index);
			user.m_UserId = X360Core.GetUserOnlinePlayerId(index).Raw.ToString();
			user.m_Friends = GetFriendsList(index);
			user.m_Image = X360Core.GetUserGamerPicture(index, true);
		}

		public void Authenticate(Action<bool> callback)
		{
			// Request online login of exactly 1 user
			X360Core.RequestSignIn(1, 1, onlineMode);
			X360Core.OnUserStateChange = delegate() { callback(true); };
		}

		// Request sign-in for min users up to max user count
		public void Authenticate(uint minUsers, uint maxUsers, Action<bool> callback)
		{
			X360Core.RequestSignIn(minUsers, maxUsers, onlineMode);
			// Xbox core doesn't report success/failure of sign-in attempts
			// so lets just wrap it in another delegate which always reports true
			// I assume this means all users have signed in
			X360Core.OnUserStateChange = delegate() { callback(true); };
		}

		// This is kind of pointless here as the friends list is not loaded seperately
		public void LoadFriends(Action<bool> callback)
		{
			/*if (X360Friends.IsInitialized(0))
			{
				Debug.Log("Not initialized yet... ");
				X360Friends.OnFriendsUpdated = delegate(uint index) { callback(true); };
				return;
			}*/
			callback(true);
			Debug.Log("Friends list is always populated in the local user automatically");
		}

		private UserProfile[] GetFriendsList(uint index)
		{
			var friends = new List<UserProfile>();
			for (uint i = 0; i < X360Friends.GetFriendCount(index); i++)
			{
				UserProfile friend = new UserProfile();
				friend.m_UserName = X360Friends.GetFriendName(index, i);
				friend.m_UserId = X360Friends.GetFriendPlayerId(index, i).Raw.ToString();
				friend.m_IsFriend = true;
				X360FriendState state = X360Friends.GetFriendState(index, i);
				friend.m_State = ConvertState(state);
				friend.m_Image = X360Core.GetPlayerGamerPicture(index, X360Friends.GetFriendPlayerId(index, i), false);
				friends.Add(friend);
			}
			return friends.ToArray();
		}

		private UserState ConvertState(X360FriendState state)
		{
			if (state.IsOnline) return UserState.Online;
			if (state.IsOnlineAndAway) return UserState.OnlineAndAway;
			if (state.IsOnlineAndBusy) return UserState.OnlineAndBusy;
			if (state.IsPlaying) return UserState.Playing;
			return UserState.Offline;
		}

		// Achievements are set up with the Xbox 360 and LIVE Authoring Submission Tool (XLAST)
		// The points (or gamescore) of each one depends on the title type (retail/arcade).
		// They are actually loaded automatically at startup, so this doesn't actually load
		// anything.
		public void LoadAchievementDescriptions(Action<AchievementDescription[]> callback)
		{
			if (!X360Achievements.IsEnumerated())
			{
				m_AchievementDescriptionCallback = callback;
				X360Achievements.OnAchievementsEnumerated = CallbackAchivementDescriptionLoader;
			}
			else if (X360Achievements.GetCount() == 0)
			{
				Debug.Log("No achievement descriptions found");
				callback(new AchievementDescription[0]);
			}
			else
			{
				callback(PopulateAchievementDescriptions());
			}
		}

		private void CallbackAchivementDescriptionLoader()
		{
			if (m_AchievementDescriptionCallback != null)
				m_AchievementDescriptionCallback(PopulateAchievementDescriptions());
		}

		private AchievementDescription[] PopulateAchievementDescriptions()
		{
			var achievements = new List<AchievementDescription>();
			for (uint i = 0; i < X360Achievements.GetCount(); ++i)
			{
				// TODO: Should the points maybe just be an uint like xbox uses? Not like you get negative points ever.
				X360Achievement xboxAchoo = new X360Achievement(i);
				AchievementDescription achievement = new AchievementDescription(
					xboxAchoo.Id.ToString(),
					xboxAchoo.Label,
					xboxAchoo.Picture,
					xboxAchoo.Description,
					xboxAchoo.Unachieved,
					xboxAchoo.ShowUnachieved,
					(int)xboxAchoo.Cred);
				achievements.Add(achievement);
			}
			return achievements.ToArray();
		}

		// Apparently xbox has no concept of unhiding achievements by reporting 0 progress
		// TODO: This is printed in the log:
		//		 '[XUI] Warning: XuiControlPlayOptionalVisual: no fallback for control: PopupControl.  Trying to play: "Normal"->"EndNormal"'
		public void ReportProgress(string id, double progress, Action<bool> callback)
		{
			uint numericId;
			if (!XboxLiveUtil.TryExtractId(id, out numericId)) return;

			ReportProgress(numericId, progress, callback);
		}

		public void ReportProgress(uint id, double progress, Action<bool> callback)
		{
			m_AchievementReportingCallback = callback;
			X360Achievements.OnAward = CallbackAchivementReported;
			X360Achievements.AwardUser(kDefaultUserIndex, id);
		}

		private void CallbackAchivementReported(uint userIndex, uint achievementId, X360AchievementStatus status)
		{
			// TODO: Check if the desired achievement ID actually got updated.
			// TODO: We should have enums here instead of bools
			if (m_AchievementReportingCallback != null)
			{
				if (status == X360AchievementStatus.AlreadyAwarded || status == X360AchievementStatus.Succeeded)
					m_AchievementReportingCallback(true);
				else
					m_AchievementReportingCallback(false);
			}
		}

		public void LoadAchievements(Action<Achievement[]> callback)
		{
			// TODO: This should really be communicated back with an enum, but since users can re-enumerate
			// it's kind of useless. We should allow that or automatically handle it.
			if (!X360Achievements.IsEnumerated())
			{
				Debug.Log("Achievements not yet enumerated");
				callback(new Achievement[0]);
			}
			if (X360Achievements.GetCount() == 0)
			{
				Debug.Log("No achievements found or achieved");
				callback(new Achievement[0]);
			}
			else
			{
				callback(PopulateAchievements(kDefaultUserIndex));
			}
		}

		private Achievement[] PopulateAchievements(uint index)
		{
			var achievements = new List<Achievement>();
			for (uint i = 0; i < X360Achievements.GetCount(); ++i)
			{
				X360Achievement xboxAchoo = new X360Achievement(i);
				if (X360Achievements.IsUnlocked(index, xboxAchoo.Id, true))
				{
					Achievement achievement = new Achievement(
						xboxAchoo.Id.ToString(),
						100.0,
						true,
						false,
						X360Achievements.GetUnlockTime(index, xboxAchoo.Id));
					achievements.Add(achievement);
				}
			}
			return achievements.ToArray();
		}

		// Got this:
		// WRN[XGI]: Mismatched types for property 0x10000001. XUSER_PROPERTY.value.type = 2 but property type is 1. Skipping write. Pass the correct type or 0.
		public void ReportScore(long score, string board, Action<bool> callback)
		{
			uint boardId;
			if (!XboxLiveUtil.TryExtractId(board, out boardId)) return;
			
			// There must be a Score property assigned to this leaderboard and it must expect a 64 bit value
			uint propertyId;
			if (XboxLiveUtil.TryExtractId("PROPERTY_SCORE", out propertyId))
			{
				Debug.Log("Found Score property: " + propertyId);
				var properties = new X360UserProperty[1];
				properties[0].Id = propertyId;
				properties[0].Value.Type = X360UserDataType.Int64;
				properties[0].Value.ValueInt64 = score;
				ReportScore(boardId, properties, callback);
			}
			else
				Debug.LogError("Failed to report score to " + board);
		}

		// TODO: The API is set up so you have one leaderboard object, and muliple ones if you need to report/read
		//       to/from multiple leaderboards. Xbox has the X360StatsViewProperties value which allows you to report
		//       scores etc to multiple leaderboard views at a time. Here we always have one such property.
		public void ReportScore(uint boardId, X360UserProperty[] props, Action<bool> callback)
		{
			if (X360Core.GetTotalOnlineUsers() == 0)
			{
				Debug.Log("ERROR: Leaderboards can only be used when the user is logged in online (online=" + X360Core.GetTotalOnlineUsers() + " signed-in=" + X360Core.GetTotalSignedInUsers() + ")");
				callback(false);
				return;
			}

			LeaderboardRoutine routine = GetRoutine();
			XboxScore xboxScore = new XboxScore();
			xboxScore.boardId = boardId;
			xboxScore.properties = props;
			routine.ReportScore(xboxScore, callback);
		}

		public void LoadScores(string category, Action<Score[]> callback)
		{
			uint categoryId;
			if (!XboxLiveUtil.TryExtractId(category, out categoryId)) return;

			// This will find all columns assigned to this leaderboard, if specific columns are desired
			// you need to use the xbox specific LoadScores call which has the columns parameter
			ushort[] columnIds;
			if (XboxLiveUtil.TryExtractId("STATS_COLUMN_" + category.ToUpper(), out columnIds))
				LoadScores(categoryId, columnIds, callback);
			else
				Debug.LogError("Failed to load scores from " + category);
		}

		// We convert X360StatsRow + X360StatsColumn objects to Score objects.
		// NOTE: Each row contains user info + all the columns for him. Here we will only support the user info + 1 single column (value/score/points)
		// NOTE: We only support getting long (64bit) values, not floats etc.
		// NOTE: The date field in the Score class is not populated, not supported here unless we support it as a custom column in the leaderboard config.
		// TODO: Fix formattedValue field, it should be possible to populate based on the localized string accociated with a column
		// TODO: Unused row fields: Rating + Gamertag
		// NOTE: Arbitrated leaderboards + the TrueSkill system complicates matters here. An
		//		 arbitrated session must be started to be able to report scores on an arbitrated leaderboard.
		//		 When using TrueSkill, every player must report all scores (also for other players) and the server
		//		 verifies they are correct.
		public void LoadScores(uint boardId, ushort[] columnIds, Action<Score[]> callback)
		{
			LeaderboardRoutine routine = GetRoutine();
			routine.LoadScores(boardId, columnIds, callback);
		}

		private LeaderboardRoutine GetRoutine()
		{
			LeaderboardRoutine routine;
			if (s_LeaderboardRoutine == null)
			{
				s_LeaderboardRoutine = new GameObject();
				routine = s_LeaderboardRoutine.AddComponent<LeaderboardRoutine>();
			}
			else
				routine = s_LeaderboardRoutine.GetComponent<LeaderboardRoutine>();
			return routine;
		}

		internal static XboxLiveSession GetSession()
		{
			XboxLiveSession session;
			if (s_SessionObject == null)
			{
				Debug.Log("Creating new session object");
				s_SessionObject = new GameObject();
				session = s_SessionObject.AddComponent<XboxLiveSession>();
			}
			else
			{
				Debug.Log("Reusing old session");
				session = s_SessionObject.GetComponent<XboxLiveSession>();
			}
			return session;
		}

		public void LoadScores(Leaderboard board, Action<bool> callback)
		{
			board.m_Loading = true;
			var routine = GetRoutine();
			routine.LoadScores((XboxLeaderboard) board, callback);
		}

		public bool GetLoading(Leaderboard board)
		{
			return board.m_Loading;
		}

		public static void OpenSession(Action<bool> callback)
		{
			XboxLiveSession session = GetSession();

			if (session.running)
			{
				callback(true);
				return;
			}

			session.SetupSession(callback);
		}

		public static void CloseSession(Action<bool> callback)
		{
			if (s_SessionObject == null)
			{
				callback(true);
				return;
			}
			
			var routine = s_SessionObject.GetComponent<XboxLiveSession>();
			routine.Cleanup(callback);
		}
	}

	class LeaderboardRoutine : MonoBehaviour
	{
		private Action<Score[]> m_ScoresCallback;
		private Action<bool> m_LeaderboardCallback;
		// If it's possible with the Xbox SDK to do parallel leaderboard queries, then expand this into a boardID=>board hashtable
		private XboxLeaderboard m_CurrentBoard;

		public void ReportScore(XboxScore score, Action<bool> callback)
		{
			StartCoroutine(DoReportScore(score, callback));
		}

		// TODO: Public/private slots needs to be exposed somehow, but this is only relevant for multiplayer games
		// TODO: No callbacks are accociated with writes (as they happen later)?
		// TODO: Deal with this error: WRN[XGI]: Invalid leaderboard id: 0x00000000
		public IEnumerator DoReportScore(XboxScore score, Action<bool> callback)
		{
			/*yield return StartCoroutine(SetupSession());
			Debug.Log("Session setup done");
			if (m_Session == null)
			{
				callback(false);
				yield break;
			}*/
			XboxLiveSession session = XboxLive.GetSession();
			if (!session.running)
			{
				Debug.Log("Must open a session first");
				callback(false);
				yield break;
			}

			var viewProp = new X360StatsViewProperties[1];
			viewProp[0] = new X360StatsViewProperties { ViewId = score.boardId, Properties = score.properties };
			X360PlayerId onlineId = X360Core.GetUserOnlinePlayerId(XboxLive.kDefaultUserIndex);
			if (!session.activeSession.WriteStats(onlineId, viewProp))
			{
				Debug.LogError("Failed to send leaderboard data to server");
				//yield return StartCoroutine(TearDownSession());
				callback(false);
			}
			
			// DEBUG: Maybe also skip this yield
			yield return new WaitForSeconds(0.1F);
			Debug.Log(DateTime.Now + ": Waiting for session to become available.");
			yield return !session.activeSession.IsIdle();

			//Debug.Log("Flush stats");
			//m_Session.FlushStats();
			//yield return new WaitForSeconds(0.1F);
			//Debug.Log(DateTime.Now + ": Waiting for session to become available.");
			//yield return !m_Session.IsIdle();

			// TODO: Was it actually a success?
			callback(true);
		}

		public void LoadScores(uint categoryId, ushort[] columnIds, Action<Score[]> callback)
		{
			m_ScoresCallback = callback;
			XboxLeaderboard board = new XboxLeaderboard();
			board.boardId = categoryId;
			board.columnIds = columnIds;
			board.playerScope = PlayerScope.FriendsOnly;
			StartCoroutine(DoLoadScores(board));
		}

		public void LoadScores(XboxLeaderboard leaderboard, Action<bool> callback)
		{
			m_LeaderboardCallback = callback;
			m_CurrentBoard = leaderboard;

			string categoryString = "";
			if (!XboxLiveUtil.TryExtractString(leaderboard.boardId, "STATS_VIEW_", out categoryString))
				Debug.Log("Failed to set leaderboard category name");
			else
				m_CurrentBoard.category = categoryString;
			StartCoroutine(DoLoadScores(leaderboard));
		}

		public IEnumerator DoLoadScores(XboxLeaderboard board)
		{
			XboxLiveSession session = XboxLive.GetSession();
			if (!session.running)
			{
				Debug.Log("Must open a session first");
				yield break;
			}
			
			/*yield return StartCoroutine(SetupSession());
			if (m_Session == null)
			{
				Debug.Log("Session setup failed");
				FinishCallbacks(false, new Score[0]);
				yield break;
			}*/

			if (board.timeScope != TimeScope.AllTime)
				Debug.Log("Time scope filtering is not supported, scores from any time are always used.");

			// TODO: Figure out what should be supported, ReadPlayerStats only returns to score
			//       of the local player, this should go into Leaderboard.localPlayerScore
			if (board.playerScope == PlayerScope.FriendsOnly)
				Debug.Log("Friends only support not implemented yet, loading from all players");
			X360Stats.ReadLeaderboardByIndex(
				board.boardId, 
				board.columnIds,
				(uint) board.range.from,
				(uint) board.range.count,
				ProcessLeaderboardResult);

			// TODO: If the scores returned do not contain the local player score we need to fetch it
			// seperately so the localPlayerScore property can be populated.
			//X360Stats.ReadPlayerStats(board.boardId, board.columnIds, m_OnlineID, PopulateLocalPlayerScore);
			
			Debug.Log(DateTime.Now + ": Waiting for session to become available.");
			yield return !session.activeSession.IsIdle();
			if (session.activeSession.LastFunctionFailed())
			{
				Debug.Log("Failed to read leaderboard info");
				//m_Session = null;
				yield break;
			}
		}

		private void FinishCallbacks(bool result, Score[] scores)
		{
			if (m_CurrentBoard != null)
			{
				m_CurrentBoard.m_Scores = scores;
				m_CurrentBoard.m_Loading = false;
				if (m_LeaderboardCallback != null)
				{
					m_LeaderboardCallback(result);
					m_LeaderboardCallback = null;
				}
				m_CurrentBoard = null;
			}
			else if (m_ScoresCallback != null)
			{
				m_ScoresCallback(scores);
				m_ScoresCallback = null;
			}
		}

		// TODO: This currently assumes one and only one value (column) per score element
		private void ProcessLeaderboardResult(UInt32 viewId, UInt32 totalRows, X360StatsRow[] rows)
		{
			Debug.Log("Received " + rows.Length + " out of " + totalRows + " rows for id " + viewId);
			m_CurrentBoard.m_MaxRange = totalRows;
			var scores = new List<Score>();
			foreach (X360StatsRow row in rows)
			{
				string playerId = row.Xuid.ToString();
				long value = -1;
				if (row.Columns.Length >= 1)
					value = row.Columns[0].Value.ValueInt64;
				DateTime date = DateTime.Now;
				int rank = (int)row.Rank;

				Score score = new Score(viewId.ToString(), value, playerId, date, value.ToString(), rank);
				scores.Add(score);
			}
			FinishCallbacks(true, scores.ToArray());
		}
	}

	public class XboxScore : Score
	{
		public X360UserProperty[] properties { get; set; }
		public uint boardId { get; set; }
	}

	public class XboxLeaderboard : Leaderboard
	{
		public uint boardId { get; set; }
		public ushort[] columnIds { get; set; }

		public XboxLeaderboard()
		{
			boardId = 0;
			columnIds = new ushort[0];
		}

		public override string  ToString()
		{
			return base.ToString() + " BoardID: '" + boardId + "' ColumnIds: '" + columnIds.Length + "'";
		}
	}


	internal class XboxLiveSession : MonoBehaviour
	{
		private X360Session m_Session;
		private bool m_SessionRunning;
		private X360PlayerId m_OnlineID;
		private bool m_HazError;

		internal bool running { get { return m_SessionRunning; } }
		internal X360Session activeSession { get { return m_Session; } }
		
		XboxLiveSession()
		{
			Debug.Log("XboxLiveSession GO created");
			// Get the online ID, sanity checks is performed earlier to ensure that the user is actually online
			m_OnlineID = X360Core.GetUserOnlinePlayerId(XboxLive.kDefaultUserIndex);
		}

		public void Cleanup(Action<bool> callback)
		{
			StartCoroutine(TearDownSession(callback));
		}

		internal void SetupSession(System.Action<bool> callback = null)
		{
			StartCoroutine(DoSetupSession(callback));
		}

		// TODO: These sessions calls should all be returning success boolean...
		// TODO: Deal with this error: WRN[XGI]: User at index 0 is already a member of a presence session.
		private IEnumerator DoSetupSession(System.Action<bool> callback)
		{
			if (m_SessionRunning)
			{
				Debug.Log("Skip session creation as we already have one running");
				yield break;
			}

			m_HazError = false;

			if (m_Session == null || m_Session.IsDead())
			{
				Debug.Log(DateTime.Now + ": Creating session.");
				m_Session = X360Session.CreateSinglePlayerSessionWithStats(XboxLive.kDefaultUserIndex, 4, 0);
				if (m_Session == null) yield break;
			}
			yield return StartCoroutine(ValidateOperation("Session creation failed", callback));
			if (m_HazError) yield break;

			Debug.Log(DateTime.Now + ": Session is ready. Joining.");
			if (!m_Session.Join(m_OnlineID, false))
			{
				Debug.LogError("Failed to join session");
				m_Session = null;
				yield break;
			}
			yield return StartCoroutine(ValidateOperation("Session joining failed", callback));
			if (m_HazError) yield break;

			Debug.Log(DateTime.Now + ": Session is ready. Starting.");
			m_Session.Start();
			yield return StartCoroutine(ValidateOperation("Session failed to start", callback));
			if (m_HazError) yield break;

			Debug.Log("Session now running");
			m_SessionRunning = true;
			if (callback != null) callback(true);
		}

		// We do not delete the running session completely but only end it's operation. It will
		// be reused if OpenSession is called again.
		private IEnumerator TearDownSession(Action<bool> callback = null)
		{
			if (m_Session == null || m_Session.IsDead())
			{
				if (callback != null) callback(true);
				yield break;
			}
			m_HazError = false;

			Debug.Log("Leaving running session.");
			m_Session.Leave(m_OnlineID, false);
			yield return StartCoroutine(ValidateOperation("Failed to leave session", callback));
			if (m_HazError) yield break;

			Debug.Log("End running session");
			m_Session.End();
			yield return StartCoroutine(ValidateOperation("Failed to end session", callback));
			if (m_HazError) yield break;

			//Debug.Log("Deleting running session.");
			//m_Session.Delete();
			//yield return StartCoroutine(ValidateOperation("Failed to delete session", callback));
			//if (m_HazError) yield break;

			m_SessionRunning = false;
			Debug.Log("Successfully tore down session.");
			if (callback != null) callback(true);
		}

		private IEnumerator ValidateOperation(string error, Action<bool> callback)
		{
			//Debug.Log(DateTime.Now + ": Waiting for session to become available.");
			yield return !m_Session.IsIdle();
			if (m_Session.LastFunctionFailed())
			{
				Debug.Log(error);
				m_Session = null;
				if (callback != null) callback(false);
				m_HazError = true;
				yield break;
			}
		}
	}


	static internal class XboxLiveUtil
	{
		// TODO: Also look out for javascript/boo assemblies
		internal static Assembly s_UserScriptingAssembly = Assembly.Load(new AssemblyName("Assembly-CSharp"));

		static internal bool TryExtractId(string id, out ushort[] shorts)
		{
			List<ushort> foundShorts = new List<ushort>();
			bool success = false;
			foreach (FieldInfo info in GetSpaConfigFields())
			{
				//Debug.Log("Haz " + info);
				if (info.Name.Contains(id.ToUpper()))
				{
					// The reflected enum contains uint values, so it must first be cast to that type
					ushort value = (ushort)(uint)info.GetValue(info.GetType());
					foundShorts.Add(value);
					success = true;
					//Debug.Log("Found desired ID: " + value);
				}
			}
			shorts = foundShorts.ToArray();
			if (!success) Debug.Log("Failed to find " + id + " in SPAConfig enum");
			return success;
		}

		static internal bool TryExtractId(string id, out uint numericId)
		{
			if (uint.TryParse(id, out numericId)) return true;

			foreach (FieldInfo info in GetSpaConfigFields())
			{
				//Debug.Log("Haz " + info);
				if (info.Name.Contains(id.ToUpper()))
				{
					numericId = (uint)info.GetValue(info.GetType());
					//Debug.Log("Found desired ID: " + numericId);
					return true;
				}
			}

			Debug.Log("Failed to find " + id + " in SPAConfig enum");
			return false;
		}

		static internal bool TryExtractString(uint numericId, string pattern, out string outputString)
		{
			outputString = "";
			foreach (FieldInfo info in GetSpaConfigFields())
			{
				if (info.Name.Contains(pattern) && (uint)info.GetValue(info.GetType()) == numericId)
				{
					outputString = info.Name;
					return true;
				}
			}

			Debug.Log("Failed to find " + pattern + " field in SPAConfig enum which matched " + numericId);
			return false;
		}

		static internal FieldInfo[] GetSpaConfigFields()
		{
			if (s_UserScriptingAssembly == null) return new FieldInfo[0];
			object spaObject = s_UserScriptingAssembly.CreateInstance("spaconfig", true);
			if (spaObject == null) return new FieldInfo[0];
			Type spaType = spaObject.GetType();
			FieldInfo[] spaFields = spaType.GetFields();
			return spaFields;
		}	
	}

#endif
}
